1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module code_checker.engine.builtin.clang_tidy_classification;
7 
8 public import code_checker.engine.types : Severity;
9 
10 version (unittest) {
11     import unit_threaded : shouldEqual, shouldBeTrue;
12 }
13 
14 @safe:
15 
16 struct SeverityColor {
17     import colorlog : Color, Background, Mode;
18 
19     Color c = Color.white;
20     Background bg = Background.black;
21     Mode m;
22 }
23 
24 immutable Severity[string] diagnosticSeverity;
25 immutable SeverityColor[Severity] severityColor;
26 
27 shared static this() @trusted {
28     import logger = std.experimental.logger;
29     import std.algorithm : filter;
30     import std.array : empty;
31     import std.conv : to;
32     import std.file : thisExePath, readText;
33     import std.json : parseJSON, JSONType;
34     import std.path : buildPath, dirName;
35     import std..string : split, toLower, startsWith;
36 
37     const clangTidyPath = buildPath(thisExePath.dirName.dirName, "etc",
38             "code_checker", "clang-tidy.json");
39     auto json = parseJSON(readText(clangTidyPath));
40 
41     import colorlog : Color, Background, Mode;
42 
43     foreach (a; json["labels"].object.byKeyValue.filter!(a => a.value.type == JSONType.ARRAY)) {
44         Severity s = () {
45             foreach (v; a.value.array) {
46                 auto splt = v.str.split(":");
47                 if (!splt.empty && splt[0] == "severity") {
48                     try {
49                         auto r = splt[1].toLower.to!Severity;
50                         return r;
51                     } catch (Exception e) {
52                     }
53                 }
54             }
55             return Severity.min;
56         }();
57 
58         if (a.key.startsWith("core."))
59             diagnosticSeverity["clang-analyzer-" ~ a.key] = s;
60         else
61             diagnosticSeverity[a.key] = s;
62     }
63 
64     // dfmt off
65     severityColor = [
66         Severity.style: SeverityColor(Color.lightCyan, Background.black, Mode.none),
67         Severity.low: SeverityColor(Color.lightBlue, Background.black, Mode.bold),
68         Severity.medium: SeverityColor(Color.lightYellow, Background.black, Mode.none),
69         Severity.high: SeverityColor(Color.red, Background.black, Mode.bold),
70         Severity.critical: SeverityColor(Color.magenta, Background.black, Mode.bold),
71     ];
72     // dfmt on
73 }
74 
75 struct CountErrorsResult {
76     import code_checker.engine.types : Severity;
77 
78     private {
79         int total;
80         int[Severity] score_;
81         int suppressedWarnings;
82     }
83 
84     /// Returns: the score when summing up the found occurancies.
85     int score() @safe pure nothrow const @nogc scope {
86         int sum;
87         // just chose some numbers. The intent is that warnings should be a high penalty
88         foreach (kv; score_.byKeyValue) {
89             final switch (kv.key) {
90             case Severity.style:
91                 sum -= kv.value;
92                 break;
93             case Severity.low:
94                 sum -= kv.value * 2;
95                 break;
96             case Severity.medium:
97                 sum -= kv.value * 5;
98                 break;
99             case Severity.high:
100                 sum -= kv.value * 10;
101                 break;
102             case Severity.critical:
103                 sum -= kv.value * 100;
104                 break;
105             }
106         }
107 
108         return sum;
109     }
110 
111     void put(const Severity s) {
112         total++;
113 
114         if (auto v = s in score_)
115             (*v)++;
116         else
117             score_[s] = 1;
118     }
119 
120     void setSuppressed(const int v) {
121         suppressedWarnings = v;
122     }
123 
124     auto toRange() const {
125         import std.algorithm : map, sort;
126         import std.array : array;
127         import std.format : format;
128 
129         return score_.byKeyValue
130             .array
131             .sort!((a, b) => a.key > b.key)
132             .map!(a => format("%s %s", a.value, a.key));
133     }
134 }
135 
136 @("shall sort the error counts")
137 unittest {
138     import std.traits : EnumMembers;
139     import code_checker.engine.types : Severity;
140     import unit_threaded;
141 
142     CountErrorsResult r;
143     foreach (s; [EnumMembers!Severity])
144         r.put(s);
145 
146     r.toRange.shouldEqual([
147             "1 critical", "1 high", "1 medium", "1 low", "1 style"
148             ]);
149 }
150 
151 struct DiagMessage {
152     Severity severity;
153 
154     /// Filename that clang-tidy reported for the warning.
155     string file;
156     /// The diagnostic message such as file.cpp:2:3 error: some text [foo-check]
157     string diagnostic;
158     /// The trailing info such as fixits
159     string[] trailing;
160 }
161 
162 struct StatMessage {
163     // Number of NOLINTs
164     int nolint;
165 }
166 
167 /** Apply `fn` on the diagnostic messages.
168  *
169  * The return value from fn replaces the message. This makes it possible to
170  * rewrite a message if needed.
171  *
172  * Params:
173  *  diagFn = mapped onto a diagnostic message
174  *  statFn = statistics gathered from clang-tidy
175  *  lines = an input range of lines to analyze for diagnostic messages
176  *  w = output range that the resulting log is written to.
177  */
178 void mapClangTidy(alias diagFn, Writer)(string[] lines, ref scope Writer w) {
179     import std.algorithm : startsWith;
180     import std.array : appender, Appender;
181     import std.conv : to;
182     import std.exception : ifThrown;
183     import std.range : put;
184     import std.regex : regex, matchFirst;
185     import std..string : startsWith;
186 
187     void callDiagFnAndReset(ref DiagMessage msg, ref Appender!(string[]) app) {
188         msg.trailing = app.data;
189         app.clear;
190         if (diagFn(msg)) {
191             put(w, msg.diagnostic);
192             foreach (t; msg.trailing)
193                 put(w, t);
194         }
195 
196         msg = DiagMessage.init;
197     }
198 
199     const re_error = regex(
200             `(?P<file>.*):\d*:\d*:.*(?P<kind>(error|warning)):.*\[(?P<severity>.*)\]`);
201 
202     enum State {
203         none,
204         match,
205         partOfMatch,
206         newMatch,
207     }
208 
209     State st;
210     auto app = appender!(string[])();
211     DiagMessage msg;
212     foreach (l; lines) {
213         auto m_error = matchFirst(l, re_error);
214 
215         final switch (st) {
216         case State.none:
217             if (m_error.length > 1)
218                 st = State.match;
219             break;
220         case State.match:
221             if (m_error.length > 1)
222                 st = State.newMatch;
223             else
224                 st = State.partOfMatch;
225             break;
226         case State.partOfMatch:
227             if (m_error.length > 1)
228                 st = State.newMatch;
229             break;
230         case State.newMatch:
231             if (m_error.length <= 1)
232                 st = State.partOfMatch;
233             break;
234         }
235 
236         final switch (st) {
237         case State.none:
238             break;
239         case State.match:
240             msg.severity = classify(m_error["severity"], m_error["kind"]);
241             msg.diagnostic = l;
242             msg.file = m_error["file"];
243             break;
244         case State.partOfMatch:
245             app.put(l);
246             break;
247         case State.newMatch:
248             callDiagFnAndReset(msg, app);
249 
250             msg.severity = classify(m_error["severity"], m_error["kind"]);
251             msg.diagnostic = l;
252             msg.file = m_error["file"];
253             break;
254         }
255     }
256 
257     msg.trailing = app.data;
258     if (st != State.none && diagFn(msg)) {
259         put(w, msg.diagnostic);
260         foreach (t; msg.trailing)
261             put(w, t);
262     }
263 }
264 
265 void mapClangTidyStats(alias statFn)(string[] lines) {
266     import std.conv : to;
267     import std.exception : ifThrown;
268     import std.regex : regex, matchFirst;
269 
270     const re_nolint = regex(`Supp.*\D(?P<nolint>\d+)\s*NOLINT.*`);
271 
272     foreach (l; lines) {
273         auto m_nolint = matchFirst(l, re_nolint);
274 
275         if (m_nolint.length > 1) {
276             auto nolint_cnt = m_nolint["nolint"].to!int.ifThrown(0);
277             statFn(StatMessage(nolint_cnt));
278         }
279     }
280 }
281 
282 @("shall filter warnings")
283 unittest {
284     import std.algorithm : startsWith;
285     import std.array : appender;
286 
287     // dfmt off
288     string[] lines = [
289         "gmock-matchers.h:3410:15: error: invalid case style for private method 'AnalyzeElements' [readability-identifier-naming,-warnings-as-errors]",
290         "  MatchMatrix AnalyzeElements(ElementIter elem_first, ElementIter elem_last,",
291         "              ^~~~~~~~~~~~~~~~",
292         "              analyzeElements",
293         "gmock-matchers.h:3410:43: error: invalid case style for parameter 'elem_first' [readability-identifier-naming,-warnings-as-errors]",
294         "  MatchMatrix AnalyzeElements(ElementIter elem_first, ElementIter elem_last,",
295         "                                          ^~~~~~~~~~~",
296         "                                          elemFirst",
297         "gmock-matchers2.h:3410:67: error: invalid case style for parameter 'elem_last' [readability-identifier-naming,-warnings-as-errors]",
298         "  MatchMatrix AnalyzeElements(ElementIter elem_first, ElementIter elem_last,",
299         "                                                                  ^~~~~~~~~~",
300         "                                                                  elemLast",
301         ];
302     // dfmt on
303 
304     DiagMessage[] msgs;
305     bool diagMsg(DiagMessage msg) {
306         msgs ~= msg;
307         // skipping a message to see that it works
308         if (msgs.length == 1)
309             return false;
310         return true;
311     }
312 
313     auto app = appender!(string[])();
314     mapClangTidy!diagMsg(lines, app);
315 
316     msgs.length.shouldEqual(3);
317 
318     msgs[0].file.shouldEqual("gmock-matchers.h");
319     msgs[0].diagnostic.startsWith("gmock-matchers.h:3410:15:").shouldBeTrue;
320     msgs[0].trailing.length.shouldEqual(3);
321 
322     msgs[2].file.shouldEqual("gmock-matchers2.h");
323     msgs[2].diagnostic.startsWith("gmock-matchers2.h:3410:67").shouldBeTrue;
324     msgs[2].trailing.length.shouldEqual(3);
325 
326     app.data.length.shouldEqual(8);
327 }
328 
329 @("shall report the number of suppressed warnings")
330 unittest {
331     // dfmt off
332     string[] lines = [
333         "42598 warnings generated.",
334         "Suppressed 27578 warnings (27523 in non-user code, 55 NOLINT).",
335         "Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.",
336         ];
337     // dfmt on
338 
339     StatMessage stat;
340     void statFn(StatMessage msg) {
341         stat = msg;
342     }
343 
344     // act
345     mapClangTidyStats!statFn(lines);
346 
347     // assert
348     stat.nolint.shouldEqual(55);
349 }
350 
351 /// Returns: the classification of the diagnostic message.
352 Severity classify(string diagnostic_msg, string kind) {
353     import std..string : startsWith;
354 
355     if (kind == "error")
356         return Severity.critical;
357 
358     if (auto v = diagnostic_msg in diagnosticSeverity) {
359         return *v;
360     }
361 
362     // this is a fallback when new rules are added to clang-tidy but
363     // they haven't been thoroughly analyzed in
364     // `code_checker.engine.builtin.clang_tidy_classification`.
365     if (diagnostic_msg.startsWith("readability-"))
366         return Severity.style;
367     else if (diagnostic_msg.startsWith("clang-analyzer-"))
368         return Severity.high;
369 
370     return Severity.medium;
371 }
372 
373 /**
374  * Params:
375  *  predicate = param is the classification of the diagnostic message. True means that it is kept, false thrown away
376  * Returns: a range of rules to inactivate that are below `s`
377  */
378 auto filterSeverity(alias predicate)() {
379     import std.algorithm : filter, map;
380 
381     // dfmt off
382     return diagnosticSeverity
383         .byKeyValue
384         .filter!(a => predicate(a.value))
385         .map!(a => a.key);
386     // dfmt on
387 }
388 
389 /// Returns: severity as a string with colors.
390 string color(Severity s) {
391     import std.conv : to;
392     static import colorlog;
393 
394     SeverityColor sc;
395 
396     if (auto v = s in severityColor) {
397         sc = *v;
398     }
399 
400     return colorlog.color(s.to!string, sc.c).bg(sc.bg).mode(sc.m).toString;
401 }